package net.courier.remoting.server;

import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.courier.remoting.ServiceRegistry;
import net.courier.remoting.protocol.RequestDatagram;

public class MethodInvokeHelper {
	private static final int METHOD_CACHE_SIZE = 64;
	private static final int METHOD_PER_CLASS_SIZE = 32;

	private final ServiceRegistry registry;
	private final Map<String, Map<String, SoftReference<List<Method>>>> methodCache = new HashMap<String, Map<String, SoftReference<List<Method>>>>(
			METHOD_CACHE_SIZE);

	private final MethodInvokeInterceptor[] interceptors;

	public MethodInvokeHelper(ServiceRegistry registry) {
		this.registry = registry;
		if(registry != null && registry instanceof InterceptableServiceRegistry ) {
			interceptors = ((InterceptableServiceRegistry) registry).getSortedInterceptors();
		} else {
			interceptors = null;
		}
		
	}

	public Object invoke(RequestDatagram datagram) {
		String id = datagram.getId();
		
		if(id == null) {
			return null;
		}
		
		Object target = registry.getService(id);
		Method method = getMethodFromCache(id, target.getClass(), datagram.getMethodName(), datagram.getParameterTypes());
		
		return invokeMethod(target, method, datagram.getArguments());
	}

	private Method getMethodFromCache(String id, Class<?> targetClass, String methodName, Class<?>[] parameterTypes) {
		Map<String, SoftReference<List<Method>>> methodMap = methodCache.get(id);
		if (methodMap != null) {
			SoftReference<List<Method>> methodsReference = methodMap.get(methodName);
			List<Method> methods = methodsReference.get();
			Method method = searchMethod(methods, methodName, parameterTypes);
			if (method != null) {
				return method;
			}
		}

		Method method = getMatchMethod(targetClass, methodName, parameterTypes);
		cacheMethod(id, methodName, method);
		return method;
	}

	private Method searchMethod(List<Method> methods, String methodName, Class<?>[] parameterTypes) {
		Method res = null;
		int size = methods.size();
		for (int i = 0; i < size; i++) {
			Method m = methods.get(i);
			if (arrayContentsEq(parameterTypes, m.getParameterTypes())) {
				res = m;
				break;
			}
		}

		return res;
	}

	private boolean arrayContentsEq(Object[] a1, Object[] a2) {
		if (a1 == null) {
			return a2 == null || a2.length == 0;
		}

		if (a2 == null) {
			return a1.length == 0;
		}

		if (a1.length != a2.length) {
			return false;
		}

		for (int i = 0; i < a1.length; i++) {
			if (a1[i] != a2[i]) {
				return false;
			}
		}

		return true;
	}

	private void cacheMethod(String id, String methodName, Method method) {
		Map<String, SoftReference<List<Method>>> methodMap = methodCache.get(id);
		List<Method> newMethodList = new ArrayList<Method>();
		newMethodList.add(method);
		if (methodMap == null) {
			methodMap = new HashMap<String, SoftReference<List<Method>>>(METHOD_PER_CLASS_SIZE);
			SoftReference<List<Method>> methodsReference = new SoftReference<List<Method>>(newMethodList);
			methodMap.put(methodName, methodsReference);
			methodCache.put(id, methodMap);
		} else {
			SoftReference<List<Method>> methodsReference = methodMap.get(methodName);
			if (methodsReference == null || methodsReference.get() == null || methodsReference.get().isEmpty()) {
				methodsReference = new SoftReference<List<Method>>(newMethodList);
				methodMap.put(methodName, methodsReference);
			} else {
				methodsReference.get().add(method);
			}
		}
	}
	
	private Method getMatchMethod(Class<?> targetClass, String methodName, Class<?>[] parameterTypes) {
		Class<?>[] interfaceClasses = targetClass.getInterfaces();
		Method method = null;
		for(Class<?> interfaceClass : interfaceClasses) {
			method = getInterfaceMethod(interfaceClass, methodName, parameterTypes);
			if(method != null) {
				break;
			}
			if(interfaceClass.getInterfaces().length > 0) {
				method = getMatchMethod(interfaceClass, methodName, parameterTypes);
			}
			if(method != null) {
				break;
			}
		}
		return method;
	}
	

	private Method getInterfaceMethod(Class<?> interfaceClass, String methodName, Class<?>[] parameterTypes) {
		Method method = null;
		try {
			method = interfaceClass.getMethod(methodName, parameterTypes);
		} catch (NoSuchMethodException e) {
			new IllegalStateException(e);
		} catch (SecurityException e) {
			new IllegalStateException(e);
		}
		return method;
	}

	private Object invokeMethod(Object target, Method method, Object[] arguments) {
		boolean doInvoke = false;

		try {
			doInvoke = doPreInvoke(target, method, arguments);
		} catch (Throwable e) {
			new IllegalStateException(e);
		}

		if (!doInvoke) {
			return null;
		}

		Object result = null;

		try {
			method.setAccessible(true);
			result = method.invoke(target, arguments);
		} catch (IllegalAccessException e) {
			new IllegalStateException(e);
		} catch (IllegalArgumentException e) {
			new IllegalStateException(e);
		} catch (InvocationTargetException e) {
			new IllegalStateException(e);
		}

		try {
			result = doAfterInvoke(target, method, arguments, result);
		} catch (Throwable e) {
			new IllegalStateException(e);
		}

		return result;
	}

	private boolean doPreInvoke(Object target, Method method, Object[] arguments) throws Throwable {
		if (interceptors != null) {
			for (int i = 0; i < interceptors.length; i++) {
				MethodInvokeInterceptor interceptor = interceptors[i];
				if (!interceptor.beforeInvoke(target, method, arguments)) {
					return false;
				}
			}
		}
		return true;
	}

	private Object doAfterInvoke(Object target, Method method, Object[] arguments, Object result) throws Throwable {
		if (interceptors == null) {
			return null;
		}

		Object lastReturn = result;

		for (int i = interceptors.length - 1; i >= 0; i--) {
			MethodInvokeInterceptor interceptor = interceptors[i];
			lastReturn = interceptor.afterInvoke(target, method, arguments, result, lastReturn);
		}

		return lastReturn;
	}
}
